package cn.yuxirey.spotsview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;

public class SpotsView<T extends Spot> extends View {
    private Paint mPaint;
    private float spotSize;
    private float spotStrokeSize;
    private float spacing;
    private int direction;
    private int spotCount = 0;
    private int gravity = Gravity.LEFT;
    private SpotData<T> mData;
    private DataChangeObserver observer = this::notifyDataChanged;

    public SpotsView(Context context) {
        super(context);
        init(context, null);
    }

    public SpotsView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public SpotsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    public SpotsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs);
    }


    void init(Context context, AttributeSet attrs) {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SpotsView);
        spacing = typedArray.getDimensionPixelSize(R.styleable.SpotsView_spotSpacing, 0);
        spotSize = typedArray.getDimensionPixelSize(R.styleable.SpotsView_spotSize, 0);
        spotStrokeSize = typedArray.getDimensionPixelSize(R.styleable.SpotsView_spotStrokeSize, 0);
        direction = typedArray.getInt(R.styleable.SpotsView_spotDirection, Direction.HORIZONTAL);
        gravity = typedArray.getInt(R.styleable.SpotsView_spotGravity, Gravity.LEFT);
        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int measureW = MeasureSpec.getSize(widthMeasureSpec);
        int measureH = MeasureSpec.getSize(heightMeasureSpec);
        int resultW = measureW;
        int resultH = measureH;
        if (direction == Direction.VERTICAL) {
            if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
                resultH = getPaddingTop() + getPaddingBottom();
                if (spotCount > 0) {
                    resultH += getSpotTotalSize();
                }
            }
            if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) {
                resultW = (int) Math.ceil(spotSize + getPaddingLeft() + getPaddingRight() + spotStrokeSize / 2f);
            }
        } else {
            if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) {
                resultW = getPaddingLeft() + getPaddingRight();
                if (spotCount > 0) {
                    resultW += getSpotTotalSize();
                }
            }
            if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
                resultH = (int) Math.ceil(spotSize + getPaddingTop() + getPaddingBottom() + spotStrokeSize / 2f);
            }
        }
        setMeasuredDimension(resultW, resultH);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mData == null || mData.getItemCount() == 0) {
            if (BuildConfig.DEBUG) {
                Log.w("SpotsView", "Adapter set to SpotsView is null, skipped.");
            }
            return;
        }
        float rStroke = spotStrokeSize / 2f;
        float startX = 0;
        float startY = 0;
        boolean initialed = false;
        mPaint.setStrokeWidth(spotStrokeSize);

        for (Spot spot : mData.getDataList()) {
            if (spot == null) {
                continue;
            }
            float drawRStroke = SpotType.STROKE == (spot.getType() | SpotType.STROKE) ? rStroke : 0.0f;
            if (!initialed) {
                startX = getStartX();
                startY = getStartY();
                initialed = true;
            }
//            // 绘制边框
            if (drawRStroke > 0) {
                mPaint.setColor(spot.getBorderColor());
                mPaint.setStyle(Paint.Style.STROKE);
                canvas.drawArc(startX + drawRStroke,
                        startY + drawRStroke,
                        startX + spotSize - drawRStroke,
                        startY + spotSize - drawRStroke,
                        0, 360,
                        false, mPaint);
            }
            // 绘制圆心
            if (spot.getSolid() != Color.TRANSPARENT && spot.getSolid() != 0) {
                mPaint.setColor(spot.getSolid());
                mPaint.setStrokeWidth(spotStrokeSize);
                mPaint.setStyle(Paint.Style.FILL);
                canvas.drawArc(startX + drawRStroke * 2,
                        startY + drawRStroke * 2,
                        startX + (spotSize - drawRStroke * 2),
                        startY + (spotSize - drawRStroke * 2),
                        0, 360,
                        false, mPaint);
            }
            if (direction == Direction.HORIZONTAL) {
                startX = startX + spotSize + spacing;
            } else {
                startY = startY + spotSize + spacing;
            }
        }
    }

    public void setSpotSize(int spotSize) {
        this.spotSize = spotSize;
        invalidate();
    }

    public void setSpacing(int spacing) {
        this.spacing = spacing;
        invalidate();
    }

    public void setSpotStrokeSize(int strokeSize) {
        this.spotStrokeSize = strokeSize;
        invalidate();
    }

    public void setDirection(@DirectionType int direction) {
        this.direction = direction;
        notifyDataChanged();
    }

    @SpotGravity
    public int getGravity() {
        return gravity;
    }

    public void setGravity(@SpotGravity int gravity) {
        this.gravity = gravity;
    }

    public void bindData(SpotData<T> spotData) {
        mData = spotData;
        if (mData == null) {
            if (BuildConfig.DEBUG) {
                Log.w("SpotsView", "SpotsView adapter is null.");
            }
            return;
        }
        mData.setDataChangedObserver(observer);
        spotCount = mData.getItemCount();
    }

    public int getSpacing() {
        return (int) Math.ceil(spacing);
    }

    @DirectionType
    public int getDirection() {
        return direction;
    }

    public static class Direction {
        public final static int HORIZONTAL = 1;
        public final static int VERTICAL = 2;
    }

    @IntDef({
            Direction.HORIZONTAL,
            Direction.VERTICAL
    })
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.SOURCE)
    public @interface DirectionType {

    }

    @IntDef({
            Gravity.LEFT,
            Gravity.TOP,
            Gravity.RIGHT,
            Gravity.BOTTOM,
            Gravity.CENTER,
            Gravity.LEFT | Gravity.TOP,
            Gravity.LEFT | Gravity.CENTER,
            Gravity.LEFT | Gravity.BOTTOM,
            Gravity.RIGHT | Gravity.TOP,
            Gravity.RIGHT | Gravity.CENTER,
            Gravity.RIGHT | Gravity.BOTTOM,
            Gravity.TOP | Gravity.CENTER,
            Gravity.BOTTOM | Gravity.CENTER,
    })
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SpotGravity {

    }

    public static class Gravity {
        public final static int LEFT = 0x02;
        public final static int TOP = 0x01;
        public final static int RIGHT = 0x04;
        public final static int BOTTOM = 0x08;
        public final static int CENTER = 0x10;

        public static boolean same(int source, int target) {
            return target == (source & target);
        }

    }

    private void notifyDataChanged() {
        spotCount=mData==null?0:mData.getItemCount();
        post(new Runnable() {
            @Override
            public void run() {
                if (!isInLayout()) {
                    requestLayout();
                }
            }
        });
    }

    private float getStartX() {
        if (Gravity.same(gravity, Gravity.LEFT)) {
            return getPaddingLeft();
        }
        if (Gravity.same(gravity, Gravity.RIGHT)) {
            if (direction == Direction.VERTICAL) {
                return getPaddingLeft() + (getMeasuredWidth() - getPaddingBottom() - spotSize);
            }
            return getMeasuredWidth() - getSpotTotalSize() + getPaddingLeft();
        }
        if (Gravity.same(gravity, Gravity.CENTER)) {
            if (direction == Direction.VERTICAL) {
                return (getMeasuredWidth() - spotSize) / 2f + getPaddingTop();
            }
            return (getMeasuredWidth() - getSpotTotalSize()) / 2f + getPaddingLeft();
        }

        return getPaddingLeft();
    }


    private float getStartY() {
        if (Gravity.same(gravity, Gravity.TOP)) {
            return getPaddingTop();
        }

        if (Gravity.same(gravity, Gravity.BOTTOM)) {
            if (direction == Direction.HORIZONTAL) {
                return getPaddingTop() + (getMeasuredHeight() - getPaddingBottom() - spotSize);
            }
            return getMeasuredHeight() - getSpotTotalSize() + getPaddingTop();
        }

        if (Gravity.same(gravity, Gravity.CENTER)) {
            if (direction == Direction.HORIZONTAL) {
                return (getMeasuredHeight() - spotSize) / 2f + getPaddingTop();
            }
            return (getMeasuredHeight() - getSpotTotalSize()) / 2f + getPaddingTop();
        }
        return getPaddingTop();
    }

    private float getSpotTotalSize() {
        return spotCount * spotSize + (spotCount - 1) * spacing;
    }
}
